#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @Project ：project
# @FileName：common.py
# @IDE     ：PyCharm
# @Author  ：叶鸟翼
# @Time    ：2022/10/9 09:52
import json
import os.path
import re
import sys
from typing import Optional, Union

import requests
from assertpy import soft_assertions, assert_that
from jsonpath import jsonpath
from loguru import logger

from settings import LOG_PATH
from utils.request import Request
from debugtalk import *


def log():
    logger.remove()
    os.makedirs(LOG_PATH, exist_ok=True)
    log_file = os.path.join(LOG_PATH, '{time}.log')
    logger.add(sink=sys.stdout, level='DEBUG', colorize=True)
    logger.add(sink=log_file, level='INFO', rotation='100 KB', retention='7 days', compression='zip')


log()


def to_dict(obj: str):
    """json str --> Python object

    :param obj:
    :return:
    :rtype: Union[list, dict]
    """
    return json.loads(obj)


def to_json(obj: Union[list, dict]):
    """Python object --> json str

    :param obj:
    :return:
    :rtype: str
    """
    return json.dumps(obj, indent=4, ensure_ascii=False)


def isfunction(func: str):
    """是否为用户自定义函数

    :param func:
    :return:
    :rtype: bool
    """
    pattern = re.compile(r"([^()]+?)\((.*?)\)")
    match = pattern.search(func)
    if match is not None:
        return True

    return False


def rep_expr(obj: dict, mapping: dict):
    """替换动态参数(${xxx})

    :param obj:
    :param mapping:
    :return:
    :rtype: dict
    """
    content = json.dumps(obj)
    pattern = re.compile(r"\${([^${}]+?)}")

    # Helper function for .sub()
    def convert(mo):
        named = mo.group(1)
        if named is not None:
            if isfunction(named):
                return str(eval(named))
            else:
                try:
                    return str(mapping[named])
                except KeyError:
                    return mo.group()
        raise ValueError('Unrecognized named group in pattern', pattern)

    # 处理类似${xxx${yyy}}的嵌套
    history = content
    while True:
        # 替换引用参数${xxx}
        content = pattern.sub(convert, history)
        # 如果本次替换后的字符产和历史上次结果字符串相同即结束循环
        if history == content:
            break

        # 记录本次替换后的结果字符串
        history = content

    obj = json.loads(content)
    return obj


def extract_by_jsonpath(obj, expr: str):
    """根据jsonpath提取字典中的value

    :param obj:
    :param expr: 表达式
    :return:
        1) jsonpath提取成功返回(True, 提取结果)
        2) jsonpath提取失败返回(False, 原表达式)
    :rtype: Tuple[bool, any]
    """
    result = jsonpath(dict(obj), expr)
    if result is not False:
        flag, repl = True, result[0]
    else:
        flag, repl = False, expr
    return flag, repl


def extract_by_regexp(obj, expr):
    """根据regular expression提取真实value

    :param obj:
    :param expr: 表达式
    :return:
        1) regexp提取成功返回(True, 提取结果)
        2) regexp提取失败返回(False, 原表达式)
    :rtype: Tuple[bool, any]
    """
    if not isinstance(expr, str):
        return False, expr

    string = json.dumps(dict(obj))
    pattern = re.compile(expr)
    match = pattern.search(string)
    if match is not None:
        try:
            flag, repl = True, match.group(1)
        except IndexError:
            flag, repl = True, match.group()
    else:
        flag, repl = False, expr
    return flag, repl


class CaseManager(object):
    """测试用例管理中心"""

    global_context = {}  # 全局上下文来处理接口请求依赖

    def __init__(self,
                 id: int,
                 name: str,
                 describe: str,
                 request: dict,
                 validate: dict,
                 extract: Optional[dict] = None,
                 skip: bool = False
                 ):
        self.id: int = id
        self.name: str = name
        self.describe: str = describe
        self.skip: bool = True if skip else False
        self.request = request if request else {}
        self.extract = extract if extract else {}
        self.validate = validate if validate else {}

        self.response = requests.Response()

    def _extract_dynamic_params_from_request(self, expr: str):
        """从请求报文进行提取

        :param expr: 表达式
        :return:
            1) 请求报文提取成功返回(True, 提取结果)
            2) 请求报文提取失败返回(False, 原表达式)
        :rtype: Tuple[bool, any]
        """
        # 优先jsonpath提取
        flag, repl = extract_by_jsonpath(self.request, expr)
        if flag is True:
            return flag, repl

        # jsonpath提取失败尝试正则提取
        flag, repl = extract_by_regexp(self.request, expr)
        # 不论正则是否提取成功都返回
        return flag, repl

    def _extract_dynamic_params_from_response(self, expr: str):
        """从响应报文进行替换

        :param expr: 表达式
        :return:
            1) 响应报文提取成功返回(True, 提取结果)
            2) 响应报文提取失败返回(False, 原表达式)
        :rtype: Tuple[bool, any]
        """
        # 优先jsonpath提取
        # 响应头提取
        flag, repl = extract_by_jsonpath(self.response.headers, expr)
        if flag is True:
            return flag, repl
        # 响应体提取
        flag, repl = extract_by_jsonpath(self.response.json(), expr)
        if flag is True:
            return flag, repl

        # jsonpath提取失败尝试正则提取
        flag, repl = extract_by_regexp(self.response.headers, expr)
        if flag is True:
            return flag, repl
        flag, repl = extract_by_regexp(self.response.json(), expr)
        # 不论正则是否提取成功都返回
        return flag, repl

    def _extract_dynamic_params(self):
        """jsonpath 或 regexp 提取(类似Jmeter的json提取器或正则提取器)

        提取顺序:
        1) 请求报文优先提取, 如果未找到再从响应报文中提取
        2) 优先jsonpath提取, 如果jsonpath提取失败采用正则提取
        3) 如果以上2条均提取失败, 则会被当成常量constant进行直接存储
        :return:
        """
        case_id = str(self.id)
        for key, value in self.extract.items():
            if not value:
                logger.warning(f"{key}: {value}, value is not exists, skip do not process...")
                continue

            key = case_id + '-' + key

            flag, value = self._extract_dynamic_params_from_request(value)
            if flag is True:
                self.global_context[key] = value
                continue

            flag, value = self._extract_dynamic_params_from_response(value)
            self.global_context[key] = value

    @staticmethod
    def _compare(actual, expect, compare):
        """ 判断用例中断言的实际结果和预期结果

        :param actual:
        :param expect:
        :param compare:
        :return:
        """
        if compare == '==':
            assert_that(actual).is_equal_to(expect)
        elif compare == "!=":
            assert_that(actual).is_not_equal_to(expect)
        elif compare == ">":
            assert_that(actual).is_greater_than(expect)
        elif compare == ">=":
            assert_that(actual).is_greater_than_or_equal_to(expect)
        elif compare == '<':
            assert_that(actual).is_less_than(expect)
        elif compare == '<=':
            assert_that(actual).is_less_than_or_equal_to(expect)
        elif compare == 'len':  # 字符串长度
            assert_that(actual).is_length(expect)
        elif compare == 'in':  # 包含关系
            assert_that(actual).contains(expect)
        elif compare == 're':  # 正则匹配
            assert_that(actual).matches(expect)
        elif compare == 'startswith':  # 匹配开头
            assert_that(actual).starts_with(expect)
        elif compare == 'endswith':  # 匹配结尾
            assert_that(actual).ends_with(expect)
        elif compare == 'typeof':  # 类型比较
            assert_that(actual).is_type_of(expect)

    def _checkpoint(self):
        """断言

        :return:
        """
        with soft_assertions():
            for key, value in self.validate.items():
                if not value:
                    logger.warning(f"{key}: {value}, value not exists!")
                    continue

                if key == 'status_code':
                    assert_that(self.response.status_code).is_equal_to(value)
                    continue

                actual, expect, compare = value.values()
                _, _expect = self._extract_dynamic_params_from_request(expect)
                _, _actual = self._extract_dynamic_params_from_response(actual)
                logger.info(f"\nThe actual result of '{actual}' is {_actual}, "
                            f"the expected result of '{expect}' is {_expect}")

                self._compare(_actual, _expect, compare)

    def execute(self):
        """用例执行

        :return:
        """
        if self.skip is True:  # 跳过skip是true的用例
            return

        # 替换请求部分可能存在的动态参数(${xxx})
        self.request = rep_expr(self.request, self.global_context)
        self.extract = rep_expr(self.extract, self.global_context)

        # 用例开始执行，发送请求
        self.response = Request(**self.request).send_request()
        log_msg = f"\n======================================================\n" \
                  f"用例ID: {self.id}\n" \
                  f"用例名称: {self.name}\n" \
                  f"用例描述: {self.describe}\n" \
                  f"请求报文: {to_json(self.request)}\n" \
                  f"响应头: {to_json(dict(self.response.headers))}\n" \
                  f"响应体: {to_json(self.response.json())}\n" \
                  f"======================================================\n"
        logger.info(log_msg)

        self._extract_dynamic_params()
        logger.info(f"全局上下文内容如下:\n{to_json(self.global_context)}")

        # 替换validate可能存在的动态参数(${xxx})
        self.validate = rep_expr(self.validate, self.global_context)
        self._checkpoint()
        logger.success("本条测试用例通过\n")
